package com.autonavi.abs.common.util;

import org.apache.commons.collections4.MapUtils;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContextBuilder;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.entity.mime.MultipartEntity;
import org.apache.http.entity.mime.content.FileBody;
import org.apache.http.entity.mime.content.StringBody;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.HttpRequestExecutor;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.KeyStore;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @author wwy
 * @version V1.0
 * <p>Description: http/https请求工具类</p>
 * @date 2020-07-21 14:32
 */
public class HttpClientUtils {

    private static final Logger LOGGER = LoggerFactory.getLogger(HttpClientUtils.class);

    /**
     * 默认utf8
     */
    public static final String Default_Charset = "UTF-8";
    /**
     * 默认json
     */
    public static final String Default_Content_Type = ContentType.APPLICATION_JSON.getMimeType();

    /**
     * 默认链接超时时长
     */
    public static final int Connection_Timeout = 1000 * 60;
    /**
     * 默认读超时时长
     */
    public static final int Read_Timeout = 1000 * 60;
    /**
     * 默认获取连接超时时长
     */
    public static final int Connection_Request_Timeout = 1000 * 10;
    /**
     * 连接最大数量
     */
    public static final int Max_Total = 400;
    /**
     * 每一个路由（ip+port）最大的连接数
     */
    public static final int Max_Route = 200;
    /**
     * 重试次数
     */
    public static final int Retry_Times = 5;
    /**
     * 默认连接空闲时间
     */
    public static final int Default_Keep_Alive_Time = 1000 * 10;
    /**
     * 连接最长空闲时间
     */
    public static final int Max_Keep_Alive_Time = 1000 * 30;
    /**
     * 空闲连接清理周期
     */
    public static final int Idle_Connection_Scan = 1000 * 5;
    private static final Object syncLock = new Object();
    private static HttpClient httpClient = null;
    private static boolean needInit = true;
    /**
     * 认证key Map
     */
    private static Map<String, KeyStoreEntry> keyStoreEntryMap = new HashMap<>();

    /**
     * url form 请求
     *
     * @param url
     * @param params
     * @return
     * @throws IOException
     */
    public static String postUrlForm(String url, Map<String, Object> params, boolean log) throws IOException {
        return postUrlForm(url, params, log, null);
    }

    /**
     * json 请求
     *
     * @param url
     * @param params
     * @return
     * @throws IOException
     */
    public static String postJson(String url, String params, boolean log) throws IOException {
        return postJson(url, params, log, null);
    }

    /**
     * x-www-form-urlencoded 请求
     *
     * @param url
     * @param params
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    public static String postUrlForm(String url, Map<String, Object> params, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig(httpClientRequestConfig));
        setUrlFormParams(httpPost, params, httpClientRequestConfig);
        return doPost(url, httpPost, log, httpClientRequestConfig);
    }

    /**
     * json 请求
     *
     * @param url
     * @param params
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    public static String postJson(String url, String params, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig(httpClientRequestConfig));
        setJsonParam(httpPost, params, httpClientRequestConfig);
        return doPost(url, httpPost, log, httpClientRequestConfig);
    }

    /**
     * 普通form请求
     *
     * @param url
     * @param params kv参数
     * @return
     * @throws IOException
     */
    public static String postForm(String url, Map<String, Object> params, boolean log) throws IOException {
        return postForm(url, params, log, null);
    }

    /**
     * 普通form请求
     *
     * @param url
     * @param params                  kv参数
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    public static String postForm(String url, Map<String, Object> params, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig(httpClientRequestConfig));
        setFormParam(httpPost, params, null, httpClientRequestConfig);
        return doPost(url, httpPost, log, httpClientRequestConfig);
    }

    /**
     * 带文件的form请求
     *
     * @param url
     * @param params                  kv参数
     * @param fileParams              文件参数
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    public static String postForm(String url, Map<String, Object> params, Map<String, File> fileParams, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig(httpClientRequestConfig));
        setFormParam(httpPost, params, fileParams, httpClientRequestConfig);
        return doPost(url, httpPost, log, httpClientRequestConfig);
    }

    /**
     * 二进制请求
     *
     * @param url
     * @param byteArray
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    public static String postBytes(String url, byte[] byteArray, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.setConfig(getRequestConfig(httpClientRequestConfig));
        setByteParam(httpPost, byteArray, httpClientRequestConfig);
        return doPost(url, httpPost, log, httpClientRequestConfig);
    }

    /**
     * do post
     *
     * @param httpPost
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    private static String doPost(String url, HttpPost httpPost, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {

        long start = System.currentTimeMillis();
        HttpResponse response = null;
        try {

            if (log) {
                LOGGER.info("HttpRequestUtils.post,url:{},start:{}", url, start);
            }

            response = getHttpClient().execute(httpPost, HttpClientContext.create());
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity, httpClientRequestConfig == null ? Default_Charset : httpClientRequestConfig.charset);

            if (log) {
                long end = System.currentTimeMillis();
                LOGGER.info("HttpRequestUtils.post,url:{},start:{},end:{},cost:{},response:{}", url, start, end, end - start, result);
            }

            return result;
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                if (response != null) {
                    EntityUtils.consume(response.getEntity());
                }
            } catch (IOException e) {

            }
        }
    }

    /**
     * url get
     *
     * @param url
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    public static String get(String url, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {
        return doGet(url, log, httpClientRequestConfig);
    }

    /**
     * get
     *
     * @param url
     * @return
     * @throws IOException
     */
    public static String get(String url, boolean log) throws IOException {
        return doGet(url, log, null);
    }

    /**
     * do get
     *
     * @param url
     * @param httpClientRequestConfig
     * @return
     * @throws IOException
     */
    private static String doGet(String url, boolean log, HttpClientRequestConfig httpClientRequestConfig) throws IOException {

        long start = System.currentTimeMillis();
        HttpResponse response = null;

        try {

            if (log) {
                LOGGER.info("HttpRequestUtils.get,url:{},start:{}", url, start);
            }


            HttpGet httpGet = new HttpGet(url);
            httpGet.setConfig(getRequestConfig(httpClientRequestConfig));

            response = getHttpClient().execute(httpGet, HttpClientContext.create());
            HttpEntity entity = response.getEntity();
            String result = EntityUtils.toString(entity, httpClientRequestConfig == null
                    ? Default_Charset : httpClientRequestConfig.charset);

            if (log) {
                long end = System.currentTimeMillis();
                LOGGER.info("HttpRequestUtils.get,url:{},start:{},end:{},cost:{},response:{}", url, start, end, end - start, result);
            }

            return result;
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                if (response != null) {
                    EntityUtils.consume(response.getEntity());
                }
            } catch (IOException e) {

            }
        }
    }

    /**
     * 增加认证key
     *
     * @param alias
     * @param keyStore
     * @param keyPassword
     */
    public static void addKeyStore(String alias, KeyStore keyStore, char[] keyPassword) {
        if (!keyStoreEntryMap.containsKey(alias)) {
            KeyStoreEntry keyStoreEntry = new KeyStoreEntry(keyStore, keyPassword);
            keyStoreEntryMap.put(alias, keyStoreEntry);
            needInit = true;
        }
    }

    /**
     * 获取连接
     *
     * @return
     */
    public static HttpClient getHttpClient() {
        if (httpClient == null || needInit) {
            synchronized (syncLock) {
                if (httpClient == null || needInit) {
                    httpClient = createHttpClient();
                    needInit = false;
                }
            }
        }
        return httpClient;
    }

    /**
     * 创建client
     *
     * @return
     */
    private static CloseableHttpClient createHttpClient() {
        CloseableHttpClient client = HttpClientBuilder.create()
                .setConnectionManager(getConnectionManager())
                .setDefaultRequestConfig(getRequestConfig())
                .setRetryHandler(getRetryHandler())
                .setKeepAliveStrategy(getKeepAliveStrategy())
                .setRequestExecutor(getHttpRequestExecutor())
                .build();
        return client;
    }

    /**
     * 连接池
     *
     * @return
     */
    private static HttpClientConnectionManager getConnectionManager() {
        // Enable Https
        SSLContext sslContext = null;
        try {
            SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
            sslContextBuilder = sslContextBuilder.loadTrustMaterial(null, new TrustStrategy() {
                @Override
                public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
                    return true;
                }
            });

            for (KeyStoreEntry keyStoreEntry : keyStoreEntryMap.values()) {
                sslContextBuilder = sslContextBuilder.loadKeyMaterial(keyStoreEntry.keyStore, keyStoreEntry.keyPassword);
            }

            sslContext = sslContextBuilder.build();
        } catch (Exception e) {
        }
        SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext, new AllowAllHostnameVerifier());

        // Enable Http
        PlainConnectionSocketFactory plainSocketFactory = PlainConnectionSocketFactory.getSocketFactory();

        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", plainSocketFactory)
                .register("https", sslSocketFactory)
                .build();

        PoolingHttpClientConnectionManager httpClientConnectionManager;
        httpClientConnectionManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);

        httpClientConnectionManager.setMaxTotal(Max_Total);
        httpClientConnectionManager.setDefaultMaxPerRoute(Max_Route);
        new IdleConnectionMonitorThread(httpClientConnectionManager).start();

        return httpClientConnectionManager;
    }

    private static RequestConfig getRequestConfig() {
        return getRequestConfig(null);
    }

    /**
     * 获取请求配置
     *
     * @param httpClientRequestConfig
     * @return
     */
    private static RequestConfig getRequestConfig(HttpClientRequestConfig httpClientRequestConfig) {

        RequestConfig.Builder builder = RequestConfig.custom();
        if (httpClientRequestConfig == null) {
            builder.setConnectTimeout(Connection_Timeout)
                    .setSocketTimeout(Read_Timeout)
                    .setConnectionRequestTimeout(Connection_Request_Timeout);

            RequestConfig requestConfig = builder.build();
            return requestConfig;
        }

        if (httpClientRequestConfig.getConnectionTimeout() == null) {
            builder.setConnectTimeout(Connection_Timeout);
        } else {
            builder.setConnectTimeout(httpClientRequestConfig.getConnectionTimeout());
        }
        if (httpClientRequestConfig.getReadTimeout() == null) {
            builder.setSocketTimeout(Read_Timeout);
        } else {
            builder.setSocketTimeout(httpClientRequestConfig.getReadTimeout());
        }
        if (httpClientRequestConfig.getConnectionRequestTimeout() == null) {
            builder.setConnectionRequestTimeout(Connection_Request_Timeout);
        } else {
            builder.setConnectionRequestTimeout(httpClientRequestConfig.getConnectionRequestTimeout());
        }

        RequestConfig requestConfig = builder.build();
        return requestConfig;
    }

    /**
     * 重试
     *
     * @return
     */
    private static HttpRequestRetryHandler getRetryHandler() {
        HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() {

            @Override
            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
                if (executionCount >= Retry_Times) {
                    return false;
                }
                if (exception instanceof NoHttpResponseException) {
                    return true;
                }
                if (exception instanceof SSLHandshakeException) {
                    return false;
                }
                if (exception instanceof InterruptedIOException) {
                    return false;
                }
                if (exception instanceof UnknownHostException) {
                    return false;
                }
                if (exception instanceof ConnectTimeoutException) {
                    return false;
                }
                if (exception instanceof SSLException) {
                    return false;
                }
                HttpClientContext clientContext = HttpClientContext.adapt(context);
                HttpRequest request = clientContext.getRequest();
                if (!(request instanceof HttpEntityEnclosingRequest)) {// 如果请求是幂等的，就再次尝试
                    return true;
                }
                return false;
            }
        };
        return httpRequestRetryHandler;
    }

    /**
     * 连接保持策略
     *
     * @return
     */
    private static ConnectionKeepAliveStrategy getKeepAliveStrategy() {
        ConnectionKeepAliveStrategy connectionKeepAliveStrategy = new ConnectionKeepAliveStrategy() {
            @Override
            public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
                HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
                while (it.hasNext()) {
                    HeaderElement he = it.nextElement();
                    String param = he.getName();
                    String value = he.getValue();
                    if (value != null && param.equalsIgnoreCase("timeout")) {
                        try {
                            return Long.parseLong(value) * 1000;
                        } catch (NumberFormatException ignore) {
                        }
                    }
                }
                return Default_Keep_Alive_Time;
            }
        };
        return connectionKeepAliveStrategy;
    }

    private static HttpRequestExecutor getHttpRequestExecutor() {
        HttpRequestExecutor httpRequestExecutor;
        httpRequestExecutor = new HttpRequestExecutor();
        return httpRequestExecutor;
    }

    /**
     * 设置form参数
     *
     * @param httpPost
     * @param params
     * @param httpClientRequestConfig
     * @throws UnsupportedEncodingException
     */
    private static void setUrlFormParams(HttpPost httpPost, Map<String, Object> params, HttpClientRequestConfig
            httpClientRequestConfig) throws UnsupportedEncodingException {

        String charset = (httpClientRequestConfig == null || httpClientRequestConfig.charset == null)
                ? Default_Charset : httpClientRequestConfig.charset;
        List<NameValuePair> nameValuePairList = new ArrayList<>();
        for (String key : params.keySet()) {
            nameValuePairList.add(new BasicNameValuePair(key, params.get(key).toString()));
        }
        UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(nameValuePairList, charset);
        urlEncodedFormEntity.setContentType(ContentType.APPLICATION_FORM_URLENCODED.getMimeType());
        httpPost.setEntity(urlEncodedFormEntity);

        setPostHeader(httpPost, httpClientRequestConfig);
    }

    /**
     * 设置body param
     *
     * @param httpPost
     * @param params
     * @param httpClientRequestConfig
     * @throws UnsupportedEncodingException
     */
    private static void setJsonParam(HttpPost httpPost, String params, HttpClientRequestConfig
            httpClientRequestConfig) {
        String charset = (httpClientRequestConfig == null || httpClientRequestConfig.charset == null)
                ? Default_Charset : httpClientRequestConfig.charset;
        String contentType = (httpClientRequestConfig == null || httpClientRequestConfig.contentType == null)
                ? Default_Content_Type : httpClientRequestConfig.contentType;

        StringEntity stringEntity = new StringEntity(params, charset);
        stringEntity.setContentType(contentType);
        httpPost.setEntity(stringEntity);

        setPostHeader(httpPost, httpClientRequestConfig);
    }

    /**
     * 设置form参数
     *
     * @param httpPost
     * @param params
     * @param fileParams
     * @param httpClientRequestConfig
     * @throws UnsupportedEncodingException
     */
    private static void setFormParam(HttpPost httpPost, Map<String, Object> params, Map<String, File> fileParams
            , HttpClientRequestConfig httpClientRequestConfig) throws UnsupportedEncodingException {

        MultipartEntity multipartEntity = new MultipartEntity();

        if (MapUtils.isNotEmpty(params)) {
            for (String key : params.keySet()) {
                multipartEntity.addPart(key, new StringBody(params.get(key).toString(), Charset.forName("utf-8")));
            }
        }

        if (MapUtils.isNotEmpty(fileParams)) {
            for (String key : fileParams.keySet()) {
                multipartEntity.addPart(key, new FileBody(fileParams.get(key)));
            }
        }

        httpPost.setEntity(multipartEntity);

        setPostHeader(httpPost, httpClientRequestConfig);
    }

    /**
     * 设置二进制文件参数
     *
     * @param httpPost
     * @param byteArray
     * @param httpClientRequestConfig
     * @throws UnsupportedEncodingException
     */
    private static void setByteParam(HttpPost httpPost, byte[] byteArray, HttpClientRequestConfig
            httpClientRequestConfig) {
        ByteArrayEntity byteArrayEntity = new ByteArrayEntity(byteArray, ContentType.APPLICATION_OCTET_STREAM);

        httpPost.setEntity(byteArrayEntity);

        setPostHeader(httpPost, httpClientRequestConfig);
    }

    /**
     * 设置header
     *
     * @param httpPost
     * @param httpClientRequestConfig
     */
    private static void setPostHeader(HttpPost httpPost, HttpClientRequestConfig httpClientRequestConfig) {
        if (httpClientRequestConfig != null && httpClientRequestConfig.getHeaderParam() != null) {
            for (Map.Entry<String, String> en : httpClientRequestConfig.getHeaderParam().entrySet()) {
                httpPost.addHeader(en.getKey(), en.getValue());
            }
        }
    }

    /**
     * 空闲连接监控线程
     */
    private static class IdleConnectionMonitorThread extends Thread {
        private final HttpClientConnectionManager connectionManager;
        private volatile boolean shutdown;

        public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) {
            super();
            this.connectionManager = connMgr;
        }

        @Override
        public void run() {
            try {
                while (!shutdown) {
                    synchronized (this) {
                        wait(Idle_Connection_Scan);
                        connectionManager.closeExpiredConnections();
                        connectionManager.closeIdleConnections(Max_Keep_Alive_Time, TimeUnit.MILLISECONDS);
                    }
                }
            } catch (InterruptedException e) {

            }
        }

        public void shutdown() {
            shutdown = true;
            synchronized (this) {
                notifyAll();
            }
        }
    }

    private static class KeyStoreEntry {
        private KeyStore keyStore;
        private char[] keyPassword;

        public KeyStoreEntry(KeyStore keyStore, char[] keyPassword) {
            this.keyStore = keyStore;
            this.keyPassword = keyPassword;
        }
    }

    public static class HttpClientRequestConfig {
        private Integer connectionTimeout;
        private Integer readTimeout;
        private Integer connectionRequestTimeout;
        private String charset;
        private String contentType;
        private Map<String, String> headerParam;

        public HttpClientRequestConfig(int connectionTimeout, int readTimeout, int connectionRequestTimeout) {
            this.connectionTimeout = connectionTimeout;
            this.readTimeout = readTimeout;
            this.connectionRequestTimeout = connectionRequestTimeout;
        }

        public Integer getConnectionTimeout() {
            return connectionTimeout;
        }

        public void setConnectionTimeout(Integer connectionTimeout) {
            this.connectionTimeout = connectionTimeout;
        }

        public Integer getReadTimeout() {
            return readTimeout;
        }

        public void setReadTimeout(Integer readTimeout) {
            this.readTimeout = readTimeout;
        }

        public Integer getConnectionRequestTimeout() {
            return connectionRequestTimeout;
        }

        public void setConnectionRequestTimeout(Integer connectionRequestTimeout) {
            this.connectionRequestTimeout = connectionRequestTimeout;
        }

        public String getCharset() {
            return charset;
        }

        public void setCharset(String charset) {
            this.charset = charset;
        }

        public String getContentType() {
            return contentType;
        }

        public void setContentType(String contentType) {
            this.contentType = contentType;
        }

        public Map<String, String> getHeaderParam() {
            return headerParam;
        }

        public void setHeaderParam(Map<String, String> headerParam) {
            this.headerParam = headerParam;
        }
    }
}
